﻿/*
	Copyright (c) 2013 Andrew Meyer

	Permission is hereby granted, free of charge, to any person obtaining a copy of this software
	and associated documentation files (the "Software"), to deal in the Software without
	restriction, including without limitation the right to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or
	substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
	BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows.Media.Media3D;

namespace Ascend
{
	/// <summary>
	/// Class that acts as a transformation basis for deformations related to an Ascend.Armature.
	/// This class cannot be inherited.
	/// </summary>
	public sealed class Bone : TransformableObject
	{
		#region Lifecycle
		/// <summary>
		/// Initializes a new instance of the Ascend.Bone class.
		/// </summary>
		public Bone()
		{
			// Create children model group
			Data.Children.Add(new Model3DGroup());

			// Create child Bone collection. Each element will correspond to a model in the children
			// model group.
			Children = new BoneCollection();

			// Create attached SceneNode model group
			Data.Children.Add(new Model3DGroup());

			// Create attached SceneNode collection. Each element will correspond to a model in the
			// attached SceneNode model group.
			RigidAttachments = new SceneNodeCollection();

			// Listen to changes in children and attached nodes
			Children.CollectionChanged += Children_CollectionChanged;
			RigidAttachments.CollectionChanged += Children_CollectionChanged;

			VertexInfluences = new List<VertexInfluence>();

			SetupRestTransforms();
		}
		#endregion

		#region Private Members
		private TranslateTransform3D _translateTransformRest;
		private RotateTransform3D _rotateTransformXRest;
		private RotateTransform3D _rotateTransformYRest;
		private RotateTransform3D _rotateTransformZRest;
		private ScaleTransform3D _scaleTransformRest;
		
		private Model3DCollection VisualChildren
		{
			get
			{
				return ((Model3DGroup)Data.Children[0]).Children;
			}
		}
		#endregion

		#region Internal Members
		internal Transform3DGroup _restTransform;

		internal Model3DCollection AttachedVisualChildren
		{
			get
			{
				return ((Model3DGroup)Data.Children[1]).Children;
			}
		}
		#endregion

		#region Public Properties
		internal Armature _armature;
		/// <summary>
		/// Gets or sets the Ascend.Armature to which this Ascend.Bone belongs.
		/// </summary>
		/// <remarks>
		/// When getting this value, if this Ascend.Bone does not belong to an Ascend.Armature's
		/// RootBones collection, the Ascend.Armature to which this Ascend.Bone's parent belongs
		/// will be returned, if any.
		/// </remarks>
		public Armature Armature
		{
			get
			{
				if (Parent == null)
				{
					return _armature;
				}

				return Parent.Armature;
			}
		}

		/// <summary>
		/// Gets or sets this Ascend.Bone's parent.
		/// </summary>
		public Bone Parent
		{
			get
			{
				return _transformParent as Bone;
			}

			set
			{
				if (Parent == value)
				{
					return;
				}

				if (value == null)
				{
					// Remove from curent parent (will set _transformParent = null)
					Parent.Children.Remove(this);
				}
				else
				{
					// Attach to new parent (will remove from existing parent and set
					// _transformParent = value)
					value.Children.Add(this);
				}
			}
		}

		/// <summary>
		/// Gets a read-only collection of Ascend.TransformableObject instances that are transformed
		/// relative to this Ascend.Bone.
		/// </summary>
		public override ReadOnlyCollection<TransformableObject> TransformChildren
		{
			get
			{
				IEnumerable<TransformableObject> rigidAttachments =
					RigidAttachments.Cast<TransformableObject>();

				IEnumerable<TransformableObject> children = Children.Cast<TransformableObject>();

				List<TransformableObject> transChildren = 
					rigidAttachments.Concat(children).ToList();
				
				return transChildren.AsReadOnly();
			}
		}

		/// <summary>
		/// Gets this Ascend.Bone's collection of child Ascend.Bone instances.
		/// </summary>
		public BoneCollection Children
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets this Ascend.Bone's collection of rigidly-attached Ascend.SceneNode instances.
		/// </summary>
		/// <remarks>
		/// Adding an Ascend.SceneNode to this collection will nullify its Parent property.
		/// </remarks>
		public SceneNodeCollection RigidAttachments
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets this Ascend.Bone's set of influenced vertices.
		/// </summary>
		public List<VertexInfluence> VertexInfluences
		{
			get;
			private set;
		}

		private double _restX;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting X coordinate.
		/// </summary>
		public double RestX
		{
			get
			{
				return _restX;
			}
			
			set
			{
				if (_restX == value)
				{
					return;
				}

				_restX = value;
				_translateTransformRest.OffsetX = _restX;
			}
		}

		private double _restY;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Y coordinate.
		/// </summary>
		public double RestY
		{
			get { return _restY; }
			set
			{
				if (_restY == value)
				{
					return;
				}

				_restY = value;
				_translateTransformRest.OffsetY = _restY;
			}
		}

		private double _restZ;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Z coordinate.
		/// </summary>
		public double RestZ
		{
			get
			{
				return _restZ;
			}
			
			set
			{
				if (_restZ == value)
				{
					return;
				}

				_restZ = value;
				_translateTransformRest.OffsetZ = _restZ;
			}
		}

		private double _restAngleX;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting X-axis angle (roll) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double RestAngleX
		{
			get { return _restAngleX; }
			set
			{
				if (_restAngleX == value)
				{
					return;
				}

				_restAngleX = value;
				((AxisAngleRotation3D)_rotateTransformXRest.Rotation).Angle = _restAngleX;
			}
		}

		private double _restAngleY;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Y-axis angle (yaw) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double RestAngleY
		{
			get
			{
				return _restAngleY;
			}
			
			set
			{
				if (_restAngleY == value)
				{
					return;
				}

				_restAngleY = value;
				((AxisAngleRotation3D)_rotateTransformYRest.Rotation).Angle = _restAngleY;
			}
		}

		private double _restAngleZ;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Z-axis angle (pitch) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double RestAngleZ
		{
			get
			{
				return _restAngleZ;
			}
			
			set
			{
				if (_restAngleZ == value)
				{
					return;
				}

				_restAngleZ = value;
				((AxisAngleRotation3D)_rotateTransformZRest.Rotation).Angle = _restAngleZ;
			}
		}

		private double _restScaleX;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting X-axis scale factor.
		/// </summary>
		public double RestScaleX
		{
			get
			{
				return _restScaleX;
			}
			
			set
			{
				if (_restScaleX == value)
				{
					return;
				}

				_restScaleX = value;
				_scaleTransformRest.ScaleX = _restScaleX;
			}
		}

		private double _restScaleY;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Y-axis scale factor.
		/// </summary>
		public double RestScaleY
		{
			get
			{
				return _restScaleY;
			}

			set
			{
				if (_restScaleY == value)
				{
					return;
				}

				_restScaleY = value;
				_scaleTransformRest.ScaleY = _restScaleY;
			}
		}

		private double _restScaleZ;
		/// <summary>
		/// Gets or sets this Ascend.Bone's resting Z-axis scale factor.
		/// </summary>
		public double RestScaleZ
		{
			get
			{
				return _restScaleZ;
			}
			
			set
			{
				if (_restScaleZ == value)
				{
					return;
				}

				_restScaleZ = value;
				_scaleTransformRest.ScaleZ = _restScaleZ;
			}
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Creates a deep copy of this Ascend.Bone, including deep copies of all children and rigid
		/// attachments.
		/// </summary>
		/// <returns>
		/// Returns the deep copy.
		/// </returns>
		public override TransformableObject Clone()
		{
			Bone clone = new Bone
			{
				Name = Name,

				RestX = RestX,
				RestY = RestY,
				RestZ = RestZ,

				RestAngleX = RestAngleX,
				RestAngleY = RestAngleY,
				RestAngleZ = RestAngleZ,

				ScaleX = ScaleX,
				ScaleY = ScaleY,
				ScaleZ = ScaleZ
			};

			ClonePose(clone);

			foreach (VertexInfluence inf in VertexInfluences)
			{
				clone.VertexInfluences.Add(inf.Clone());
			}

			foreach (Bone bone in Children)
			{
				clone.Children.Add((Bone)bone.Clone());
			}

			foreach (SceneNode node in RigidAttachments)
			{
				clone.RigidAttachments.Add((SceneNode)node.Clone());
			}

			return clone;
		}
		#endregion

		#region Internal Methods
		internal void Rest()
		{
			X = RestX;
			Y = RestY;
			Z = RestZ;

			AngleX = RestAngleX;
			AngleY = RestAngleY;
			AngleZ = RestAngleZ;

			ScaleX = RestScaleX;
			ScaleY = RestScaleY;
			ScaleZ = RestScaleZ;
		}

		internal void ClearTransformParent()
		{
			if (Parent != null)
			{
				Parent.Children.Remove(this);
			}
			else if (Armature != null)
			{
				Armature.RootBones.Remove(this);
			}
		}
		#endregion

		#region Private Methods
		private void SetupRestTransforms()
		{
			// Setup resting translation and scale transforms
			_translateTransformRest = new TranslateTransform3D();
			_scaleTransformRest = new ScaleTransform3D();

			// Setup resting rotation transform using YZX Euler angles
			Transform3DGroup rotateTransformRest = new Transform3DGroup();
			_rotateTransformXRest =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0));
			_rotateTransformYRest =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1, 0), 0));
			_rotateTransformZRest =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), 0));
			rotateTransformRest.Children.Add(_rotateTransformYRest); // yaw
			rotateTransformRest.Children.Add(_rotateTransformZRest); // pitch
			rotateTransformRest.Children.Add(_rotateTransformXRest); // roll

			// Setup total resting transform (order of children matters)
			_restTransform = new Transform3DGroup();
			_restTransform.Children.Add(_scaleTransformRest);
			_restTransform.Children.Add(rotateTransformRest);
			_restTransform.Children.Add(_translateTransformRest);
		}
		#endregion

		#region Event Handlers
		private void Children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					HandleCollectionActionAdd(sender, e);
					break;

				case NotifyCollectionChangedAction.Remove:
					HandleCollectionActionRemove(sender, e);
					break;
			}
		}

		private void HandleCollectionActionAdd(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (sender == RigidAttachments)
			{
				foreach (SceneNode node in e.NewItems)
				{
					// Clear old parent
					node.ClearTransformParent();

					// Add data to model group
					AttachedVisualChildren.Add(node.Data);

					// Set new parent
					node._transformParent = this;
				}
			}
			else if (sender == Children)
			{
				foreach (Bone child in e.NewItems)
				{
					// Clear old parent
					child.ClearTransformParent();

					// Add data to model group
					VisualChildren.Add(child.Data);

					// Set new parent
					child._transformParent = this;
				}
			}
		}

		private void HandleCollectionActionRemove(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (sender == RigidAttachments)
			{
				foreach (SceneNode node in e.OldItems)
				{
					// Remove data from model group
					AttachedVisualChildren.Remove(node.Data);

					// Clear parent
					node._transformParent = null;
				}
			}
			else if (sender == Children)
			{
				foreach (Bone child in e.OldItems)
				{
					// Remove data from model group
					VisualChildren.Remove(child.Data);

					// Clear parent
					child._transformParent = null;
				}
			}
		}
		#endregion
	}
}
